﻿using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;

namespace ImgClip
{
    /// <summary>
    /// clip middleware
    /// </summary>
    public class ClipMiddleware
    {

        private readonly RequestDelegate _next;
        private readonly HttpPocess _httpPocess;
        private readonly ClipConfig _config;
        private readonly LocalPocess _localPocess;

        public ClipMiddleware(RequestDelegate next, HttpPocess httpPocess,
            ClipConfig config,
            LocalPocess localPocess)
        {
            _next = next;
            this._config = config;
            _httpPocess = httpPocess;
            _localPocess = localPocess;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task Invoke(HttpContext context)
        {
            string path = context.Request.Path + context.Request.QueryString;

            if (FilterUrl(path))
                return;
            //if (IsModified304(context))
            //    return;

            if (_config.UseSignature)
            {
                var res = CheckSignature(path);
                if (!res.Status)
                {
                    await WriteErrorAsync(context, "Invalid request signature error");
                    return;
                }
                path = res.Path;
            }

            var (Options, RemoteUrl) = ExtractArgument(path);
            if (String.IsNullOrEmpty(RemoteUrl))
            {
                await WriteErrorAsync(context, "The image address does not exist");
                return;
            }
            var resolve = Resolve.Create(Options, _config);
            string dir = null, name = null;
            if (_config.LocalSave)
            {
                string fullUrl = GetFullUrl(RemoteUrl);
                dir = _localPocess.GetDirName(fullUrl);
                name = _localPocess.Md5(fullUrl);
                string suffix = resolve?.ToString();
                if (!String.IsNullOrEmpty(suffix))
                    name += "_" + suffix;
                using var stream = _localPocess.Get(dir, name);
                if (stream != null)
                {
                    byte[] bt = new byte[stream.Length];
                    stream.Read(bt, 0, bt.Length);
                    await WriteAsync(context, bt);
                    return;
                }
            }

            using (var stream = _httpPocess.GetImage(RemoteUrl))
            {
                if (stream == null)
                    return;
                var (Format, Data) = Processing(resolve, stream);
                await WriteAsync(context, Data, Format);
                if (_config.LocalSave)
                {
                    _localPocess.SaveAsync(dir, name, Data);
                }
            }
        }

        /// <summary>
        /// Filter URL address  
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private bool FilterUrl(string path)
        {
            return String.IsNullOrEmpty(path)
                || path.Equals("/", StringComparison.InvariantCultureIgnoreCase)
                || path.Equals("/favicon.ico", StringComparison.InvariantCultureIgnoreCase)
                || !path.Contains(_config.PathPrefix, StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// 提取远程地址和参数
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private (string Options, string RemoteUrl) ExtractArgument(string path)
        {
            int i = path.IndexOf(_config.PathPrefix);
            string left_str = path[0..i];
            string right_str = path[(i + _config.PathPrefix.Length)..];
            return (left_str, right_str);
        }

        /// <summary>
        /// 签名检测
        /// 含有签名的url地址格式 signature/[options]/plain/text.jpg
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private (bool Status, string Path) CheckSignature(string path)
        {
            path = path.Trim('/');
            var i = path.IndexOf('/');
            if (i > 0)
            {
                string signature = path.Substring(0, i);
                string url = path[i..].Trim('/');
                var sign = Encryptor.HmacSha256(WebUtility.UrlEncode(url), _config.Secret);
                return (signature.Equals(sign, StringComparison.InvariantCultureIgnoreCase), url);
            }
            return (false, path);
        }

        /// <summary>
        /// 加工图片（剪裁，大小处理等操作）
        /// </summary>
        /// <param name="options"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        private (SKEncodedImageFormat Format, byte[] Data) Processing(Resolve resolve, Stream stream)
        {
            SKEncodedImageFormat format = SKEncodedImageFormat.Jpeg;
            byte[] bt = null;
            if (resolve != null)
            {
                Stream watermark = null; 
                if (resolve.IsWatermark)
                {
                    watermark = _httpPocess.GetWatermark(resolve.WmUrl);
                }
                using SkiaPocess skia = new(stream, resolve, watermark);
                using var skData = skia.FinishAsync();
                bt = skData.ToArray();
                format = skia.Format;
            }
            else
            {
                bt = new byte[stream.Length];
                stream.Read(bt, 0, bt.Length);
            }
            return (format, bt);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="content"></param>
        /// <param name="format"></param>
        /// <param name="bt"></param>
        private Task WriteAsync(HttpContext content, byte[] bt, SKEncodedImageFormat format = SKEncodedImageFormat.Jpeg)
        {
            content.Response.Headers.TryAdd("Accept-Ranges", "bytes");
            content.Response.Headers.TryAdd("Content-Type", $"image/{format.ToString().ToLower()}");
            content.Response.Headers.TryAdd("Cache-Control", $"max-age={_config.MaxAge}");
            content.Response.Headers.TryAdd("Content-Length", bt.Length.ToString());
            if (_config.UseEtag)
            {
                var dt = DateTime.UtcNow;
                string ETag = $"{Convert.ToInt32((dt - new DateTime(1970, 1, 1)).TotalSeconds):x}-{bt.Length:x}";
                content.Response.Headers.TryAdd("ETag", ETag);
                content.Response.Headers.TryAdd("Last-Modified", dt.ToString("r"));
            }
            return content.Response.Body.WriteAsync(bt, 0, bt.Length);
        }

        /// <summary>
        /// 304 is returned during the cache period
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsModified304(HttpContext context)
        {
            if (context.Request.Headers.TryGetValue("if-none-match", out StringValues _))
            {
                if (context.Request.Headers.TryGetValue("if-modified-since", out StringValues _dt))
                {
                    if (DateTime.TryParse(_dt.ToString(), out DateTime dt))
                    {
                        var time = dt.ToUniversalTime();
                        var utcNow = DateTime.UtcNow.AddSeconds(_config.MaxAge);
                        if (utcNow > time)
                        {
                            context.Response.StatusCode = StatusCodes.Status304NotModified;
                            return true;
                        }
                    }
                }
            }
            return false;
        }


        /// <summary>
        /// write error
        /// </summary>
        /// <param name="context"></param>
        /// <param name="errorMessage"></param>
        /// <returns></returns>
        private static Task WriteErrorAsync(HttpContext context, string errorMessage)
        {
            var error = new { Message = errorMessage ?? "", RequestId = context.TraceIdentifier };
            context.Response.ContentType = "application/json";
            return context.Response.WriteAsync(JsonSerializer.Serialize(error));
        }


        /// <summary>
        /// Get the full URL address
        /// </summary>
        /// <param name="requestUrl"></param>
        /// <returns></returns>
        private string GetFullUrl(string requestUrl)
        {
            if (String.IsNullOrEmpty(requestUrl))
                return null;
            if (!requestUrl.StartsWith("http", StringComparison.InvariantCultureIgnoreCase))
                requestUrl = $"{_config.BaseUrl.TrimEnd('/')}/{requestUrl.Trim('/')}";
            return requestUrl.Trim('/');
        }
    }
}
